/*
 * Created on 31-Jul-2004
 * Created by Paul Gardner
 * Copyright (C) Azureus Software, Inc, All Rights Reserved.
 *
 * This program is free software; you can redistribute it and/or
 * modify it under the terms of the GNU General Public License
 * as published by the Free Software Foundation; either version 2
 * of the License, or (at your option) any later version.
 * This program is distributed in the hope that it will be useful,
 * but WITHOUT ANY WARRANTY; without even the implied warranty of
 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
 * GNU General Public License for more details.
 * You should have received a copy of the GNU General Public License
 * along with this program; if not, write to the Free Software
 * Foundation, Inc., 59 Temple Place - Suite 330, Boston, MA  02111-1307, USA.
 *
 */

package org.gudy.azureus2.core3.disk.impl.resume;

import java.util.ArrayList;
import java.util.Arrays;
import java.util.HashMap;
import java.util.Iterator;
import java.util.List;
import java.util.Map;

import org.gudy.azureus2.core3.config.COConfigurationManager;
import org.gudy.azureus2.core3.config.ParameterListener;
import org.gudy.azureus2.core3.disk.DiskManagerCheckRequest;
import org.gudy.azureus2.core3.disk.DiskManagerCheckRequestListener;
import org.gudy.azureus2.core3.disk.DiskManagerFileInfo;
import org.gudy.azureus2.core3.disk.DiskManagerPiece;
import org.gudy.azureus2.core3.disk.DiskManagerReadRequest;
import org.gudy.azureus2.core3.disk.DiskManagerReadRequestListener;
import org.gudy.azureus2.core3.disk.DiskManagerWriteRequest;
import org.gudy.azureus2.core3.disk.DiskManagerWriteRequestListener;
import org.gudy.azureus2.core3.disk.impl.DiskManagerFileInfoImpl;
import org.gudy.azureus2.core3.disk.impl.DiskManagerImpl;
import org.gudy.azureus2.core3.disk.impl.DiskManagerRecheckInstance;
import org.gudy.azureus2.core3.disk.impl.access.DMChecker;
import org.gudy.azureus2.core3.disk.impl.piecemapper.DMPieceList;
import org.gudy.azureus2.core3.disk.impl.piecemapper.DMPieceMapEntry;
import org.gudy.azureus2.core3.download.DownloadManager;
import org.gudy.azureus2.core3.download.DownloadManagerState;
import org.gudy.azureus2.core3.logging.LogEvent;
import org.gudy.azureus2.core3.logging.LogIDs;
import org.gudy.azureus2.core3.logging.Logger;
import org.gudy.azureus2.core3.torrent.TOTorrent;
import org.gudy.azureus2.core3.util.AESemaphore;
import org.gudy.azureus2.core3.util.ByteArrayHashMap;
import org.gudy.azureus2.core3.util.Debug;
import org.gudy.azureus2.core3.util.DirectByteBuffer;

import com.aelitis.azureus.core.diskmanager.cache.CacheFileManagerException;

/**
 * @author parg
 * 
 */
public class RDResumeHandler {
    private static final LogIDs LOGID = LogIDs.DISK;

    private static final boolean TEST_RECHECK_FAILURE_HANDLING = false;

    static {
        if (TEST_RECHECK_FAILURE_HANDLING) {
            Debug.out("**** test recheck failure enabled ****");
        }
    }

    private static final byte PIECE_NOT_DONE = 0;
    private static final byte PIECE_DONE = 1;
    private static final byte PIECE_RECHECK_REQUIRED = 2;
    private static final byte PIECE_STARTED = 3;

    private static boolean use_fast_resume;
    private static boolean use_fast_resume_recheck_all;

    static {

        COConfigurationManager.addAndFireParameterListeners(new String[] { "Use Resume", "On Resume Recheck All" }, new ParameterListener() {
            public void parameterChanged(String str) {
                use_fast_resume = COConfigurationManager.getBooleanParameter("Use Resume");
                use_fast_resume_recheck_all = COConfigurationManager.getBooleanParameter("On Resume Recheck All");
            }
        });
    }

    private DiskManagerImpl disk_manager;
    private DMChecker checker;

    private volatile boolean started;
    private volatile boolean stopped;
    private volatile boolean stopped_for_close;

    private volatile boolean check_in_progress;
    private volatile boolean check_resume_was_valid;
    private volatile boolean check_is_full_check;
    private volatile boolean check_interrupted;
    private volatile int check_position;

    public RDResumeHandler(DiskManagerImpl _disk_manager, DMChecker _writer_and_checker) {
        disk_manager = _disk_manager;
        checker = _writer_and_checker;
    }

    public void start() {
        if (started) {

            Debug.out("RDResumeHandler: reuse not supported");
        }

        started = true;
    }

    public void stop(boolean closing) {
        stopped_for_close = stopped_for_close | closing; // can get in here > once during close

        if (check_in_progress) {

            check_interrupted = true;
        }

        stopped = true;
    }

    public void checkAllPieces(boolean newfiles) {
        // long start = System.currentTimeMillis();

        DiskManagerRecheckInstance recheck_inst = disk_manager.getRecheckScheduler().register(disk_manager, false);

        final AESemaphore run_sem = new AESemaphore("RDResumeHandler::checkAllPieces:runsem", 2);

        final List<DiskManagerCheckRequest> failed_pieces = new ArrayList<DiskManagerCheckRequest>();

        try {
            boolean resume_data_complete = false;

            try {
                check_in_progress = true;

                boolean resumeEnabled = use_fast_resume;

                // disable fast resume if a new file was created

                if (newfiles) {

                    resumeEnabled = false;
                }

                final AESemaphore pending_checks_sem = new AESemaphore("RD:PendingChecks");
                int pending_check_num = 0;

                DiskManagerPiece[] pieces = disk_manager.getPieces();

                // calculate the current file sizes up front for performance reasons
                DiskManagerFileInfo[] files = disk_manager.getFiles();
                Map file_sizes = new HashMap();

                for (int i = 0; i < files.length; i++) {
                    try {
                        Long len = new Long(((DiskManagerFileInfoImpl) files[i]).getCacheFile().getLength());
                        file_sizes.put(files[i], len);
                    } catch (CacheFileManagerException e) {
                        Debug.printStackTrace(e);
                    }
                }

                if (resumeEnabled) {

                    boolean resumeValid = false;

                    byte[] resume_pieces = null;

                    Map partialPieces = null;

                    Map resume_data = getResumeData();

                    if (resume_data != null) {

                        try {

                            resume_pieces = (byte[]) resume_data.get("resume data");

                            if (resume_pieces != null) {

                                if (resume_pieces.length != pieces.length) {

                                    Debug.out("Resume data array length mismatch: " + resume_pieces.length + "/" + pieces.length);

                                    resume_pieces = null;
                                }
                            }

                            partialPieces = (Map) resume_data.get("blocks");

                            resumeValid = ((Long) resume_data.get("valid")).intValue() == 1;

                            // if the torrent download is complete we don't need to invalidate the
                            // resume data

                            if (isTorrentResumeDataComplete(disk_manager.getDownloadManager().getDownloadState(), resume_data)) {

                                resume_data_complete = true;

                            } else {

                                // set it so that if we crash the NOT_DONE pieces will be
                                // rechecked

                                resume_data.put("valid", new Long(0));

                                saveResumeData(resume_data);
                            }

                        } catch (Exception ignore) {

                            // ignore.printStackTrace();
                        }
                    }

                    if (resume_pieces == null) {

                        check_is_full_check = true;

                        resumeValid = false;

                        resume_pieces = new byte[pieces.length];

                        Arrays.fill(resume_pieces, PIECE_RECHECK_REQUIRED);
                    }

                    check_resume_was_valid = resumeValid;

                    boolean recheck_all = use_fast_resume_recheck_all;

                    if (!recheck_all) {

                        // override if not much left undone

                        long total_not_done = 0;

                        int piece_size = disk_manager.getPieceLength();

                        for (int i = 0; i < pieces.length; i++) {

                            if (resume_pieces[i] != PIECE_DONE) {

                                total_not_done += piece_size;
                            }
                        }

                        if (total_not_done < 64 * 1024 * 1024) {

                            recheck_all = true;
                        }
                    }

                    if (Logger.isEnabled()) {

                        int total_not_done = 0;
                        int total_done = 0;
                        int total_started = 0;
                        int total_recheck = 0;

                        for (int i = 0; i < pieces.length; i++) {

                            byte piece_state = resume_pieces[i];

                            if (piece_state == PIECE_NOT_DONE) {
                                total_not_done++;
                            } else if (piece_state == PIECE_DONE) {
                                total_done++;
                            } else if (piece_state == PIECE_STARTED) {
                                total_started++;
                            } else {
                                total_recheck++;
                            }
                        }

                        String str =
                                "valid=" + resumeValid + ",not done=" + total_not_done + ",done=" + total_done + ",started=" + total_started
                                        + ",recheck=" + total_recheck + ",rc all=" + recheck_all + ",full=" + check_is_full_check;

                        Logger.log(new LogEvent(disk_manager, LOGID, str));
                    }

                    for (int i = 0; i < pieces.length; i++) {

                        check_position = i;

                        DiskManagerPiece dm_piece = pieces[i];

                        disk_manager.setPercentDone(((i + 1) * 1000) / disk_manager.getNbPieces());

                        boolean pieceCannotExist = false;

                        byte piece_state = resume_pieces[i];

                        // valid resume data means that the resume array correctly represents
                        // the state of pieces on disk, be they done or not

                        if (piece_state == PIECE_DONE || !resumeValid || recheck_all) {

                            // at least check that file sizes are OK for this piece to be valid

                            DMPieceList list = disk_manager.getPieceList(i);

                            for (int j = 0; j < list.size(); j++) {

                                DMPieceMapEntry entry = list.get(j);

                                Long file_size = (Long) file_sizes.get(entry.getFile());

                                if (file_size == null) {

                                    piece_state = PIECE_NOT_DONE;
                                    pieceCannotExist = true;

                                    if (Logger.isEnabled())
                                        Logger.log(new LogEvent(disk_manager, LOGID, LogEvent.LT_WARNING, "Piece #" + i + ": file is missing, "
                                                + "fails re-check."));

                                    break;
                                }

                                long expected_size = entry.getOffset() + entry.getLength();

                                if (file_size.longValue() < expected_size) {

                                    piece_state = PIECE_NOT_DONE;
                                    pieceCannotExist = true;

                                    if (Logger.isEnabled())
                                        Logger.log(new LogEvent(disk_manager, LOGID, LogEvent.LT_WARNING, "Piece #" + i
                                                + ": file is too small, fails re-check. File size = " + file_size + ", piece needs "
                                                + expected_size));

                                    break;
                                }
                            }
                        }

                        if (piece_state == PIECE_DONE) {

                            dm_piece.setDone(true);

                        } else if (piece_state == PIECE_NOT_DONE && !recheck_all) {

                            // if the piece isn't done and we haven't been asked to recheck all pieces
                            // on restart (only started pieces) then just set as not done

                        } else {

                            // We only need to recheck pieces that are marked as not-ok
                            // if the resume data is invalid or explicit recheck needed
                            if (pieceCannotExist) {
                                dm_piece.setDone(false);
                            } else if (piece_state == PIECE_RECHECK_REQUIRED || !resumeValid) {

                                run_sem.reserve();

                                while (!stopped) {

                                    if (recheck_inst.getPermission()) {

                                        break;
                                    }
                                }

                                if (stopped) {

                                    break;

                                } else {

                                    try {
                                        DiskManagerCheckRequest request = disk_manager.createCheckRequest(i, null);

                                        request.setLowPriority(true);

                                        checker.enqueueCheckRequest(request, new DiskManagerCheckRequestListener() {
                                            public void checkCompleted(DiskManagerCheckRequest request, boolean passed) {
                                                if (TEST_RECHECK_FAILURE_HANDLING && (int) (Math.random() * 10) == 0) {

                                                    disk_manager.getPiece(request.getPieceNumber()).setDone(false);

                                                    passed = false;
                                                }

                                                if (!passed) {

                                                    synchronized (failed_pieces) {

                                                        failed_pieces.add(request);
                                                    }
                                                }

                                                complete();
                                            }

                                            public void checkCancelled(DiskManagerCheckRequest request) {
                                                complete();
                                            }

                                            public void checkFailed(DiskManagerCheckRequest request, Throwable cause) {
                                                complete();
                                            }

                                            protected void complete() {
                                                run_sem.release();

                                                pending_checks_sem.release();
                                            }
                                        });

                                        pending_check_num++;

                                    } catch (Throwable e) {

                                        Debug.printStackTrace(e);
                                    }
                                }
                            }
                        }
                    }

                    while (pending_check_num > 0) {

                        pending_checks_sem.reserve();

                        pending_check_num--;
                    }

                    if (partialPieces != null) {

                        Iterator iter = partialPieces.entrySet().iterator();

                        while (iter.hasNext()) {

                            Map.Entry key = (Map.Entry) iter.next();

                            int pieceNumber = Integer.parseInt((String) key.getKey());

                            DiskManagerPiece dm_piece = pieces[pieceNumber];

                            if (!dm_piece.isDone()) {

                                List blocks = (List) partialPieces.get(key.getKey());

                                Iterator iterBlock = blocks.iterator();

                                while (iterBlock.hasNext()) {

                                    dm_piece.setWritten(((Long) iterBlock.next()).intValue());
                                }
                            }
                        }
                    }
                } else {

                    // resume not enabled, recheck everything

                    for (int i = 0; i < pieces.length; i++) {

                        check_position = i;

                        disk_manager.setPercentDone(((i + 1) * 1000) / disk_manager.getNbPieces());

                        boolean pieceCannotExist = false;

                        // check if there is an underlying file for this piece, if not set it to not done
                        DMPieceList list = disk_manager.getPieceList(i);

                        for (int j = 0; j < list.size(); j++) {
                            DMPieceMapEntry entry = list.get(j);

                            Long file_size = (Long) file_sizes.get(entry.getFile());
                            if (file_size == null) {
                                pieceCannotExist = true;
                                break;
                            }

                            long expected_size = entry.getOffset() + entry.getLength();
                            if (file_size.longValue() < expected_size) {
                                pieceCannotExist = true;
                                break;
                            }
                        }

                        if (pieceCannotExist) {
                            disk_manager.getPiece(i).setDone(false);
                            continue;
                        }

                        run_sem.reserve();

                        while (!stopped) {

                            if (recheck_inst.getPermission()) {

                                break;
                            }
                        }

                        if (stopped) {

                            break;
                        }

                        try {
                            DiskManagerCheckRequest request = disk_manager.createCheckRequest(i, null);

                            request.setLowPriority(true);

                            checker.enqueueCheckRequest(request, new DiskManagerCheckRequestListener() {
                                public void checkCompleted(DiskManagerCheckRequest request, boolean passed) {
                                    if (TEST_RECHECK_FAILURE_HANDLING && (int) (Math.random() * 10) == 0) {

                                        disk_manager.getPiece(request.getPieceNumber()).setDone(false);

                                        passed = false;
                                    }

                                    if (!passed) {

                                        synchronized (failed_pieces) {

                                            failed_pieces.add(request);
                                        }
                                    }

                                    complete();
                                }

                                public void checkCancelled(DiskManagerCheckRequest request) {
                                    complete();
                                }

                                public void checkFailed(DiskManagerCheckRequest request, Throwable cause) {
                                    complete();
                                }

                                protected void complete() {
                                    run_sem.release();

                                    pending_checks_sem.release();
                                }
                            });

                            pending_check_num++;

                        } catch (Throwable e) {

                            Debug.printStackTrace(e);
                        }
                    }

                    while (pending_check_num > 0) {

                        pending_checks_sem.reserve();

                        pending_check_num--;
                    }
                }

                if (failed_pieces.size() > 0 && !TEST_RECHECK_FAILURE_HANDLING) {

                    byte[][] piece_hashes = disk_manager.getTorrent().getPieces();

                    ByteArrayHashMap<Integer> hash_map = new ByteArrayHashMap<Integer>();

                    for (int i = 0; i < piece_hashes.length; i++) {

                        hash_map.put(piece_hashes[i], i);
                    }

                    for (DiskManagerCheckRequest request : failed_pieces) {

                        while (!stopped) {

                            if (recheck_inst.getPermission()) {

                                break;
                            }
                        }

                        if (stopped) {

                            break;
                        }

                        byte[] hash = request.getHash();

                        if (hash != null) {

                            final Integer target_index = hash_map.get(hash);

                            int current_index = request.getPieceNumber();

                            int piece_size = disk_manager.getPieceLength(current_index);

                            if (target_index != null && target_index != current_index && disk_manager.getPieceLength(target_index) == piece_size
                                    && !disk_manager.isDone(target_index)) {

                                final AESemaphore sem = new AESemaphore("PieceReorder");

                                disk_manager.enqueueReadRequest(disk_manager.createReadRequest(current_index, 0, piece_size),
                                        new DiskManagerReadRequestListener() {
                                            public void readCompleted(DiskManagerReadRequest request, DirectByteBuffer data) {
                                                try {
                                                    disk_manager.enqueueWriteRequest(disk_manager.createWriteRequest(target_index, 0, data, null),
                                                            new DiskManagerWriteRequestListener() {
                                                                public void writeCompleted(DiskManagerWriteRequest request) {
                                                                    try {
                                                                        DiskManagerCheckRequest check_request =
                                                                                disk_manager.createCheckRequest(target_index, null);

                                                                        check_request.setLowPriority(true);

                                                                        checker.enqueueCheckRequest(check_request,
                                                                                new DiskManagerCheckRequestListener() {
                                                                                    public void checkCompleted(DiskManagerCheckRequest request,
                                                                                            boolean passed) {
                                                                                        sem.release();
                                                                                    }

                                                                                    public void checkCancelled(DiskManagerCheckRequest request) {
                                                                                        sem.release();
                                                                                    }

                                                                                    public void checkFailed(DiskManagerCheckRequest request,
                                                                                            Throwable cause) {
                                                                                        sem.release();
                                                                                    }
                                                                                });
                                                                    } catch (Throwable e) {

                                                                        sem.release();
                                                                    }
                                                                }

                                                                public void writeFailed(DiskManagerWriteRequest request, Throwable cause) {
                                                                    sem.release();
                                                                }
                                                            });
                                                } catch (Throwable e) {

                                                    sem.release();
                                                }
                                            }

                                            public void readFailed(DiskManagerReadRequest request, Throwable cause) {
                                                sem.release();
                                            }

                                            public int getPriority() {
                                                return (-1);
                                            }

                                            public void requestExecuted(long bytes) {
                                            }
                                        });

                                sem.reserve();
                            }
                        }
                    }
                }
            } finally {

                check_in_progress = false;
            }

            // dump the newly built resume data to the disk/torrent

            if (!(stopped || resume_data_complete)) {

                try {
                    saveResumeData(true);

                } catch (Exception e) {

                    Debug.out("Failed to dump initial resume data to disk");

                    Debug.printStackTrace(e);
                }
            }
        } catch (Throwable e) {

            // if something went wrong then log and continue.

            Debug.printStackTrace(e);

        } finally {

            recheck_inst.unregister();

            // System.out.println( "Check of '" + disk_manager.getDownloadManager().getDisplayName() + "' completed in " + (System.currentTimeMillis()
            // - start));
        }
    }

    public void saveResumeData(boolean interim_save) // data is marked as "invalid" if this is true to enable checking on pieces on crash restart

            throws Exception {
        if (check_in_progress && interim_save) {

            // while we are rechecking it is important that an interim save doesn't come
            // along and overwite the persisted resume data. This is because should we crash
            // while rechecking we need the persisted state to be unchanged so that on
            // restart the rechecking occurs again

            // a non-interim save means that the user has decided to stop the download (or some
            // other such significant event) so we just persist the current state

            return;
        }

        // if file caching is enabled then this is an important time to ensure that the cache is
        // flushed as we are going to record details about the accuracy of written data.
        // First build the resume map from the data (as updates can still be goin on)
        // Then, flush the cache. This means that on a successful flush the built resume
        // data matches at least the valid state of the data
        // Then update the torrent

        DiskManagerFileInfo[] files = disk_manager.getFiles();

        if (!use_fast_resume) {

            // flush cache even if resume disable as this is a good point to ensure that data
            // is persisted anyway

            for (int i = 0; i < files.length; i++) {

                files[i].flushCache();
            }

            return;
        }

        boolean was_complete = isTorrentResumeDataComplete(disk_manager.getDownloadManager().getDownloadState());

        DiskManagerPiece[] pieces = disk_manager.getPieces();

        // build the piece byte[]

        byte[] resume_pieces = new byte[pieces.length];

        for (int i = 0; i < resume_pieces.length; i++) {

            DiskManagerPiece piece = pieces[i];

            // if we are terminating due to az closure and this has interrupted a recheck then
            // make sure that the recheck continues appropriately on restart

            if (stopped_for_close && check_interrupted && check_is_full_check && i >= check_position) {

                resume_pieces[i] = PIECE_RECHECK_REQUIRED;

            } else if (piece.isDone()) {

                resume_pieces[i] = PIECE_DONE;

            } else if (piece.getNbWritten() > 0) {

                resume_pieces[i] = PIECE_STARTED;

            } else {

                resume_pieces[i] = PIECE_NOT_DONE;
            }
        }

        Map resume_data = new HashMap();

        resume_data.put("resume data", resume_pieces);

        Map partialPieces = new HashMap();

        for (int i = 0; i < pieces.length; i++) {

            DiskManagerPiece piece = pieces[i];

            // save the partial pieces for any pieces that have not yet been completed
            // and are in-progress (i.e. have at least one block downloaded)

            boolean[] written = piece.getWritten();

            if ((!piece.isDone()) && piece.getNbWritten() > 0 && written != null) {

                boolean all_written = true;

                for (int j = 0; j < written.length; j++) {

                    if (!written[j]) {

                        all_written = false;

                        break;
                    }
                }

                if (all_written) {

                    // just mark the entire piece for recheck as we've stopped the torrent at the
                    // point where a check-piece was, or was about to be, scheduled

                    resume_pieces[i] = PIECE_RECHECK_REQUIRED;

                } else {

                    List blocks = new ArrayList();

                    for (int j = 0; j < written.length; j++) {

                        if (written[j]) {

                            blocks.add(new Long(j));
                        }
                    }

                    partialPieces.put("" + i, blocks);
                }
            }
        }

        resume_data.put("blocks", partialPieces);

        long lValid;

        if (check_interrupted) {

            // set validity to what it was before the check started

            lValid = check_resume_was_valid ? 1 : 0;

        } else if (interim_save) {

            // set invalid so that not-done pieces get rechecked on startup

            lValid = 0;

        } else {

            lValid = 1;
        }

        resume_data.put("valid", new Long(lValid));

        for (int i = 0; i < files.length; i++) {

            files[i].flushCache();
        }

        // OK, we've got valid resume data and flushed the cache

        boolean is_complete = isTorrentResumeDataComplete(disk_manager.getDownloadManager().getDownloadState(), resume_data);

        if (was_complete && is_complete) {

            // no change, no point in writing

        } else {

            saveResumeData(resume_data);
        }
    }

    protected Map getResumeData() {
        return (getResumeData(disk_manager.getDownloadManager()));
    }

    protected static Map getResumeData(DownloadManager download_manager) {
        return (getResumeData(download_manager.getDownloadState()));
    }

    protected static Map getResumeData(DownloadManagerState download_manager_state) {
        Map resume_map = download_manager_state.getResumeData();

        if (resume_map != null) {

            Map resume_data = (Map) resume_map.get("data");

            return (resume_data);

        } else {

            return (null);
        }
    }

    protected void saveResumeData(Map resume_data) {
        saveResumeData(disk_manager.getDownloadManager().getDownloadState(), resume_data);
    }

    protected static void saveResumeData(DownloadManagerState download_manager_state, Map resume_data) {
        Map resume_map = new HashMap();

        resume_map.put("data", resume_data);

        download_manager_state.setResumeData(resume_map);
    }

    public static void setTorrentResumeDataComplete(DownloadManagerState download_manager_state) {
        TOTorrent torrent = download_manager_state.getTorrent();

        int piece_count = torrent.getNumberOfPieces();

        byte[] resume_pieces = new byte[piece_count];

        Arrays.fill(resume_pieces, PIECE_DONE);

        Map resume_data = new HashMap();

        resume_data.put("resume data", resume_pieces);

        Map partialPieces = new HashMap();

        resume_data.put("blocks", partialPieces);

        resume_data.put("valid", new Long(1));

        saveResumeData(download_manager_state, resume_data);
    }

    protected static int clearResumeDataSupport(DownloadManager download_manager, DiskManagerFileInfo file, boolean recheck,
            boolean onlyClearUnsharedFirstLast) {
        DownloadManagerState download_manager_state = download_manager.getDownloadState();

        Map resume_data = getResumeData(download_manager);

        if (resume_data == null) {

            return (0);
        }

        int pieces_cleared = 0;

        // clear any affected pieces

        byte[] resume_pieces = (byte[]) resume_data.get("resume data");

        int firstPiece = file.getFirstPieceNumber();
        int lastPiece = file.getLastPieceNumber();

        if (onlyClearUnsharedFirstLast) {
            DiskManagerFileInfo[] files = download_manager.getDiskManagerFileInfo();
            boolean firstPieceShared = false;
            boolean lastPieceShared = false;

            int firstFile = findFirstFileWithPieceN(firstPiece, files);

            for (int i = firstFile; i < files.length; i++) {
                DiskManagerFileInfo currentFile = files[i];
                if (currentFile.getLastPieceNumber() < firstPiece)
                    continue;
                if (currentFile.getIndex() == file.getIndex())
                    continue;
                if (currentFile.getFirstPieceNumber() > lastPiece)
                    break;
                if (currentFile.getFirstPieceNumber() <= firstPiece && firstPiece <= currentFile.getLastPieceNumber())
                    firstPieceShared |= !currentFile.isSkipped();
                if (currentFile.getFirstPieceNumber() <= lastPiece && lastPiece <= currentFile.getLastPieceNumber())
                    lastPieceShared |= !currentFile.isSkipped();
            }

            if (firstPieceShared)
                firstPiece++;

            if (lastPieceShared)
                lastPiece--;
        }

        if (resume_pieces != null) {

            for (int i = firstPiece; i <= lastPiece; i++) {

                if (i >= resume_pieces.length) {

                    break;
                }

                if (resume_pieces[i] == PIECE_DONE) {

                    pieces_cleared++;
                }

                resume_pieces[i] = recheck ? PIECE_RECHECK_REQUIRED : PIECE_NOT_DONE;
            }
        }
        // clear any affected partial pieces

        Map partial_pieces = (Map) resume_data.get("blocks");

        if (partial_pieces != null) {

            Iterator iter = partial_pieces.keySet().iterator();

            while (iter.hasNext()) {

                int piece_number = Integer.parseInt((String) iter.next());

                if (piece_number >= firstPiece && piece_number <= lastPiece) {

                    iter.remove();
                }
            }
        }

        // either way we're valid as
        // 1) clear -> pieces are set as not done
        // 2) recheck -> pieces are set as "recheck" and will be checked on restart

        resume_data.put("valid", new Long(1));

        saveResumeData(download_manager_state, resume_data);

        return (pieces_cleared);
    }

    /**
     * finds the first affected file via binary search, this is necessary as some methods might be invoked for all files, which would result in O(n²)
     * if we'd scan the whole file array every time
     */
    private static int findFirstFileWithPieceN(int firstPiece, DiskManagerFileInfo[] files) {
        int start = 0;
        int end = files.length - 1;
        int pivot = 0;

        while (start <= end) {
            pivot = (start + end) >>> 1;
            int midVal = files[pivot].getLastPieceNumber();

            if (midVal < firstPiece)
                start = pivot + 1;
            else if (midVal > firstPiece)
                end = pivot - 1;
            else {
                // some matching file, now slide leftwards to find the first one, shouldn't be that many
                while (pivot > 0 && files[pivot - 1].getLastPieceNumber() == firstPiece)
                    pivot--;
                break;
            }
        }

        return pivot;
    }

    public static boolean fileMustExist(DownloadManager download_manager, DiskManagerFileInfo file) {

        Map resumeData = getResumeData(download_manager);

        byte[] resumePieces = resumeData != null ? (byte[]) resumeData.get("resume data") : null;

        boolean sharesAnyNeededPieces = false;

        DiskManagerFileInfo[] files = download_manager.getDiskManagerFileInfo();
        int firstPiece = file.getFirstPieceNumber();
        int lastPiece = file.getLastPieceNumber();

        int firstFile = findFirstFileWithPieceN(firstPiece, files);

        // we must sweep over the files, as any number of files could share the first/last piece of the file we're probing
        for (int i = firstFile; i < files.length && !sharesAnyNeededPieces; i++) {
            DiskManagerFileInfo currentFile = files[i];
            if (currentFile.getLastPieceNumber() < firstPiece)
                continue;
            if (currentFile.getIndex() == file.getIndex() && resumePieces != null && file.getStorageType() != DiskManagerFileInfo.ST_COMPACT
                    && file.getStorageType() != DiskManagerFileInfo.ST_REORDER_COMPACT)
                for (int j = firstPiece; j <= lastPiece && !sharesAnyNeededPieces; j++)
                    sharesAnyNeededPieces |= resumePieces[j] != PIECE_NOT_DONE;
            if (currentFile.getFirstPieceNumber() > lastPiece)
                break;
            if (currentFile.getFirstPieceNumber() <= firstPiece && firstPiece <= currentFile.getLastPieceNumber())
                sharesAnyNeededPieces |= !currentFile.isSkipped();
            if (currentFile.getFirstPieceNumber() <= lastPiece && lastPiece <= currentFile.getLastPieceNumber())
                sharesAnyNeededPieces |= !currentFile.isSkipped();
        }

        return sharesAnyNeededPieces;
    }

    public static int storageTypeChanged(DownloadManager download_manager, DiskManagerFileInfo file) {
        return (clearResumeDataSupport(download_manager, file, false, true));
    }

    public static void clearResumeData(DownloadManager download_manager, DiskManagerFileInfo file) {
        clearResumeDataSupport(download_manager, file, false, false);
    }

    public static void recheckFile(DownloadManager download_manager, DiskManagerFileInfo file) {
        clearResumeDataSupport(download_manager, file, true, false);
    }

    public static void setTorrentResumeDataNearlyComplete(DownloadManagerState download_manager_state) {
        // backwards compatability, resume data key is the dir

        TOTorrent torrent = download_manager_state.getTorrent();

        long piece_count = torrent.getNumberOfPieces();

        byte[] resume_pieces = new byte[(int) piece_count];

        Arrays.fill(resume_pieces, PIECE_DONE);

        // randomly clear some pieces

        for (int i = 0; i < 3; i++) {

            int piece_num = (int) (Math.random() * piece_count);

            resume_pieces[piece_num] = PIECE_RECHECK_REQUIRED;
        }

        Map resumeMap = new HashMap();

        resumeMap.put("resume data", resume_pieces);

        Map partialPieces = new HashMap();

        resumeMap.put("blocks", partialPieces);

        resumeMap.put("valid", new Long(0)); // recheck the not-done pieces

        saveResumeData(download_manager_state, resumeMap);
    }

    public static boolean isTorrentResumeDataComplete(DownloadManagerState dms) {
        // backwards compatability, resume data key is the dir

        Map resume_data = getResumeData(dms);

        return (isTorrentResumeDataComplete(dms, resume_data));
    }

    protected static boolean isTorrentResumeDataComplete(DownloadManagerState download_manager_state, Map resume_data) {
        try {
            int piece_count = download_manager_state.getTorrent().getNumberOfPieces();

            if (resume_data != null) {

                byte[] pieces = (byte[]) resume_data.get("resume data");
                Map blocks = (Map) resume_data.get("blocks");
                boolean valid = ((Long) resume_data.get("valid")).intValue() == 1;

                // any partial pieced -> not complete

                if (blocks == null || blocks.size() > 0) {

                    return (false);
                }

                if (valid && pieces != null && pieces.length == piece_count) {

                    for (int i = 0; i < pieces.length; i++) {

                        if (pieces[i] != PIECE_DONE) {

                            // missing piece or recheck outstanding

                            return (false);
                        }
                    }

                    return (true);
                }
            }
        } catch (Throwable e) {

            Debug.printStackTrace(e);
        }

        return (false);
    }
}
